
package fourbox;

import java.io.*;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import static java.nio.file.StandardWatchEventKinds.*;
import static java.nio.file.LinkOption.*;
import java.util.logging.Level;
import java.util.logging.Logger;



public class WatchDir implements Runnable {

    private final WatchService watcher;
    private final Map<WatchKey, Path> keys;
    private final boolean recursive;
    private boolean trace = false;

    @SuppressWarnings("unchecked")
    static <T> WatchEvent<T> cast(WatchEvent<?> event) {
        return (WatchEvent<T>) event;
    }
    private List queue;
    private String contextPath;

    public synchronized List getQueue() {
        return queue;
    }

    /**
     * Register the given directory with the WatchService
     */
    private void register(Path dir) throws IOException {
        WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
        if (trace) {
            Path prev = keys.get(key);
            if (prev == null) {
                System.out.format("register: %s\n", dir);
            } else {
                if (!dir.equals(prev)) {
                    System.out.format("update: %s -> %s\n", prev, dir);
                }
            }
        }
        keys.put(key, dir);
    }

    /**
     * Register the given directory, and all its sub-directories, with the
     * WatchService.
     */
    private void registerAll(final Path start) throws IOException {
        // register directory and sub-directories
        Files.walkFileTree(start, new SimpleFileVisitor<Path>() {

            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
                    throws IOException {
                register(dir);
                return FileVisitResult.CONTINUE;
            }
        });


    }

    private synchronized void metafilingAll(final Path start, final List queue) throws IOException {
        // register directory and sub-directories
        Files.walkFileTree(start, new SimpleFileVisitor<Path>() {

            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
                    throws IOException {
//                if (kind == ENTRY_CREATE || kind == ENTRY_MODIFY) {

                Metafile m = Metafile.createMetafile(contextPath, dir.toAbsolutePath().toFile());
                if (dir.toFile().exists()) {
                    m.status = Metafile.UPDATED;
                } else {
                    m.status = Metafile.DELETED;
                }
//                System.out.println("M " + m);
                queue.add(m);
//                }
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
//                System.out.println("FILEEE " + file);
//                register(file);                    
                Metafile m = Metafile.createMetafile(contextPath, file.toAbsolutePath().toFile());
                if (file.toFile().exists()) {
                    m.status = Metafile.UPDATED;
                } else {
                    m.status = Metafile.DELETED;
                }
//                System.out.println("M " + m);
                queue.add(m);
                return FileVisitResult.CONTINUE;
            }
//            @Override
//            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
//                if (kind == ENTRY_DELETE) {
//                    Metafile m = Metafile.createMetafile(contextPath, dir.toAbsolutePath().toFile());
//                    m.status = Metafile.DELETED;
//                    System.out.println("M " + m);
//                    queue.add(m);
//                }
//                return FileVisitResult.CONTINUE;
//            }
        });
    }

    /**
     * Creates a WatchService and registers the given directory
     */
    public WatchDir(Path dir, boolean recursive) throws IOException {
        this.watcher = FileSystems.getDefault().newWatchService();
        this.keys = new HashMap<WatchKey, Path>();
        this.recursive = recursive;
        this.queue = Collections.synchronizedList(new ArrayList());
        this.contextPath = dir.toAbsolutePath().toString();        

        if (recursive) {
            System.out.format("Scanning %s ...\n", dir);
            registerAll(dir);
            System.out.println("Done.");
        } else {
            register(dir);
        }

        // enable trace after initial registration
        this.trace = true;
    }

    /**
     * Process all events for keys queued to the watcher
     */
    void processEvents() {
        for (;;) {

            // wait for key to be signalled
            WatchKey key;
            try {
                key = watcher.take();
            } catch (InterruptedException x) {
                return;
            }

            Path dir = keys.get(key);
            if (dir == null) {
                System.err.println("WatchKey not recognized!!");
                continue;
            }

//            synchronized (queue) {
                for (WatchEvent<?> event : key.pollEvents()) {
                    WatchEvent.Kind kind = event.kind();

                    // TBD - provide example of how OVERFLOW event is handled
                    if (kind == OVERFLOW) {
                        continue;
                    }

                    // Context for directory entry event is the file name of entry
                    WatchEvent<Path> ev = cast(event);
                    Path name = ev.context();
                    Path child = dir.resolve(name);

                    // print out event
                    System.out.format("%s: %s\n", event.kind().name(), child);



                    Metafile m = Metafile.createMetafile(contextPath, child.toAbsolutePath().toFile());


                    if (kind == ENTRY_CREATE) {
                        m.status = Metafile.NEW;
                    } else if (kind == ENTRY_MODIFY) {
                        if (child.toFile().exists()) {
                            m.status = Metafile.UPDATED;
                        } else {
                            m.status = Metafile.DELETED;
                        }
                    } else if (kind == ENTRY_DELETE) {
                        m.status = Metafile.DELETED;
                    }


                    if (Files.isDirectory(child, NOFOLLOW_LINKS)) {
                        m.isFile = false;
                        if (kind == ENTRY_CREATE) {
                            try {                                
                                metafilingAll(child, queue);
                            } catch (IOException ex) {
                                Logger.getLogger(WatchDir.class.getName()).log(Level.SEVERE, null, ex);
                            }
                        } else if (kind == ENTRY_DELETE) {
                            queue.add(m);
                        }
                    } else {
                        m.isFile = true;
                        queue.add(m);                        
                    }

                    // if directory is created, and watching recursively, then
                    // register it and its sub-directories
                    if (recursive && (kind == ENTRY_CREATE)) {
                        try {
                            if (Files.isDirectory(child, NOFOLLOW_LINKS)) {
                                registerAll(child);

                            }
                        } catch (IOException x) {
                            // ignore to keep sample readbale
                        }
                    }
                }
//            }//sync            

            // reset key and remove from set if directory no longer accessible
            boolean valid = key.reset();
            if (!valid) {
                keys.remove(key);

                // all directories are inaccessible
                if (keys.isEmpty()) {
                    break;
                }
            }
        }
    }

    static void usage() {
        System.err.println("usage: java WatchDir [-r] dir");
        System.exit(-1);
    }

    public static void main(String[] args) throws IOException {
        // parse arguments
        if (args.length == 0 || args.length > 2) {
            usage();
        }
        boolean recursive = false;
        int dirArg = 0;
        if (args[0].equals("-r")) {
            if (args.length < 2) {
                usage();
            }
            recursive = true;
            dirArg++;
        }

        // register directory and process its events
        System.out.println("Dirarg " + dirArg);
        Path dir = Paths.get(args[dirArg]);

//        List queue = Collections.synchronizedList(new ArrayList());
        WatchDir monitor = new WatchDir(dir, recursive);
        Thread monitorThread = new Thread(monitor);
        monitorThread.start();
    }

    @Override
    public void run() {
        processEvents();
    }
}
